package com.eobard.filter;

import com.eobard.config.security.CustomUserDetailsService;
import com.eobard.exception.CustomException;
import com.eobard.handler.LoginFailureHandler;
import com.eobard.utils.JwtUtils;
import com.eobard.utils.RedisUtils;
import com.eobard.vo.TokenVo;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author Eobard_Thawne
 * @version 1.0
 * @date 2022/8/12 -18:04
 */

/**
 * 过滤器:用于过滤token的验证
 *          1):若是登录请求,则直接放行,因为没有生产token
 *          2):其余请求,一律需要携带有效的token才能放行
 */
@Component
@Data
public class CheckTokenFilter extends OncePerRequestFilter {

    //获取登录请求地址
    @Value("${request.login.url}")
    private String loginUrl;

    @Resource
    private RedisUtils redisUtils;

    @Resource
    private JwtUtils jwtUtils;

    @Resource
    private CustomUserDetailsService userDetailsService;

    @Resource
    private LoginFailureHandler loginFailureHandler;


    /**
     * 过滤请求步骤:
     *          1.获取请求路径,判断是否为登录请求;所有非登录请求必须携带token信息
     *          2.判断redis是否存在token,不存在说明token已过期
     *          3.判断redis的token和请求token是否一致,不一致说明token被盗
     *          4.解析token的的用户信息,用户信息为空,说明解析失败
     *          5.根据解析的用户信息获取用户,为空说明不存在
     *          6.将获取的用户设置对应的角色信息并添加到spring security上下文
     *          7.将过滤器交给spring security管理
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            //获取当前请求的url地址
            String url = request.getRequestURI();
            //若请求的地址和登录地址不是一个地址:则必须要验证token
            if(!url.equals(loginUrl)){
                this.validateToken(request);
            }
        } catch (AuthenticationException e) {
            loginFailureHandler.onAuthenticationFailure(request,response,e);
        }

        //说明是登录请求,则放行
        doFilter(request,response,filterChain);
    }

    //验证request中的token信息
    private void validateToken(HttpServletRequest request) {
        //获取请求头的token
        String token = request.getHeader("token");
        //如果请求头没有token,尝试从请求参数中获取
        if (ObjectUtils.isEmpty(token)){
            token = request.getParameter("token");
        }

        //若两个地方都没有token信息,则抛出异常
        if(ObjectUtils.isEmpty(token)){
            throw new CustomException("请求中未携带token！");
        }

        //判断redis中是否存在该token
        String tokenKey = TokenVo.USER_TOKEN_PREFIX +token;
        String redisToken = redisUtils.get(tokenKey);

        //如果redis里面没有token: token已经过期或不存在当前token
        if (ObjectUtils.isEmpty(redisToken)) {
            throw new CustomException("token已过期或不存在");
        }

        //如果token和Redis中的token不一致，则验证失败
        if (!token.equals(redisToken)) {
            throw new CustomException("token验证失败");
        }


        //如果存在token，则从token中解析出用户名
        String username = jwtUtils.getUsernameFromToken(token);

        //如果用户名为空，则解析失败
        if (ObjectUtils.isEmpty(username)) {
            throw new CustomException("token解析失败");
        }

        //根据用户名获取用户信息
        UserDetails userDetails =userDetailsService.loadUserByUsername(username);
        //判断用户是否为空
        if (userDetails == null) {
            throw new CustomException("token验证失败");
        }

        //创建身份验证对象
        //参数1:用户信息,参数2:密码,参数3:用户的角色信息
        UsernamePasswordAuthenticationToken authenticationToken = new
                UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

        authenticationToken.setDetails(new
                WebAuthenticationDetailsSource().buildDetails(request));
        //设置到Spring Security上下文:类似于将它放入session中,便于后面获取对应的用户
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
}
